Skip to content

Blocks 与事件监听 (Blocks and Event Listeners)

Gradio Blocks 是一种低级别 API,它允许您比 Interface API 更灵活地创建自定义的 Web 应用和 Demo。使用 Blocks,您可以更精细地控制组件的布局、触发函数执行的事件,以及数据流。

Blocks 结构

使用 Blocks 的基本方式是在一个 with gr.Blocks() as demo: 上下文管理器中定义您的应用。

python
import gradio as gr

def greet(name):
    return "Hello " + name + "!"

with gr.Blocks() as demo:
    name_textbox = gr.Textbox(label="Name")
    output_textbox = gr.Textbox(label="Output Box")
    greet_button = gr.Button("Greet")

    greet_button.click(fn=greet, inputs=name_textbox, outputs=output_textbox, api_name="greet")

demo.launch()

解读:

  • with gr.Blocks() as demo::所有 Blocks 应用的代码都包含在这个 with 语句块中。Blocks 实例通常命名为 demo
  • 组件 (Components):这些与 Interface 中使用的组件相同。然而,在 Blocks 中,组件在 with 语句块中被创建时会自动添加到应用中,而不是传递给某个构造函数。
  • 事件监听器 (Event Listeners):如 greet_button.click(...),事件监听器定义了应用内的数据流。在上面的例子中,监听器将 name_textbox(输入)和 output_textbox(输出)通过 greet 函数连接起来。当 greet_button被点击时,这个数据流就会被触发。与 Interface 类似,一个事件监听器可以有多个输入或输出。

装饰器语法

您也可以使用装饰器语法来附加事件监听器,这样可以省略 fn 参数,直接指定 inputsoutputs

python
import gradio as gr

with gr.Blocks() as demo:
    name_textbox = gr.Textbox(label="Name")
    output_textbox = gr.Textbox(label="Output Box")
    greet_button = gr.Button("Greet")

    @greet_button.click(inputs=name_textbox, outputs=output_textbox)
    def greet(name):
        return "Hello " + name + "!"

demo.launch()

事件监听器与交互性 (Event Listeners and Interactivity)

在上面的例子中,您会发现 name_textbox 是可编辑的,而 output_textbox 是不可编辑的。这是因为任何作为事件监听器输入的组件都会默认变为可交互的。相反,由于 output_textbox 仅作为输出,Gradio 会默认使其不可交互。

您可以通过组件的 interactive 布尔参数来覆盖这种默认行为,例如 gr.Textbox(interactive=True)

python
output_textbox = gr.Textbox(label="Output", interactive=True)

注意:如果一个 Gradio 组件既不是输入也不是输出,会发生什么?如果组件在构造时带有默认值,则假定它用于显示内容,并呈现为非交互式。否则,它将呈现为交互式。同样,这种行为可以通过指定 interactive 参数的值来覆盖。

事件监听器的类型 (Types of Event Listeners)

不同的组件支持不同类型的事件监听器。例如,gr.Button 支持 click 事件,gr.Textbox 支持 change (当内容改变时) 和 submit (当用户按下 Enter 键时) 事件,而 gr.Video 支持 play 事件 (当用户按下播放按钮时)。

查看下面的示例,其中 welcome 函数由在 inp 文本框中键入内容触发,这是因为使用了 change() 事件监听器:

python
import gradio as gr

def welcome(name):
    return f"Welcome to Gradio, {name}!"

with gr.Blocks() as demo:
    gr.Markdown(
    '''
    # Hello World!
    Start typing below to see the output.
    '''
    )
    inp = gr.Textbox(placeholder="What is your name?")
    out = gr.Textbox()
    inp.change(fn=welcome, inputs=inp, outputs=out)

demo.launch()

请查阅各个组件的文档以了解它们支持的特定事件监听器。

多数据流 (Multiple Data Flows)

Interface 不同,Blocks 应用不限于单一的数据流。您可以创建多个独立的数据流,连接应用中的各个组件。

python
import gradio as gr

def increase(num):
    return num + 1

with gr.Blocks() as demo:
    a_number = gr.Number(label="a")
    b_number = gr.Number(label="b")
    
    button_atob = gr.Button("a > b")
    button_btoa = gr.Button("b > a")

    button_atob.click(fn=increase, inputs=a_number, outputs=b_number)
    button_btoa.click(fn=increase, inputs=b_number, outputs=a_number)

demo.launch()

在这个例子中,a_number 可以作为 b_number 的输入,反之亦然。随着应用变得更复杂,您可能会有许多数据流连接各种组件。

下面是一个"多步骤"Demo的例子,其中一个模型(语音转文本)的输出被送入下一个模型(情感分类器):

python
from transformers import pipeline
import gradio as gr

asr_pipeline = pipeline("automatic-speech-recognition", "facebook/wav2vec2-base-960h")
classifier_pipeline = pipeline("text-classification")

def speech_to_text(speech_audio):
    if speech_audio is None:
        return "", ""
    text_output = asr_pipeline(speech_audio)["text"]
    return text_output

def text_to_sentiment(text_input):
    if not text_input:
        return ""
    return classifier_pipeline(text_input)[0]["label"]

with gr.Blocks() as demo:
    audio_file_input = gr.Audio(type="filepath", label="Upload Audio")
    text_output_stt = gr.Textbox(label="Speech to Text")
    label_output_sentiment = gr.Label(label="Sentiment")

    button_recognize = gr.Button("Recognize Speech")
    button_classify = gr.Button("Classify Sentiment")

    button_recognize.click(speech_to_text, inputs=audio_file_input, outputs=text_output_stt)
    button_classify.click(text_to_sentiment, inputs=text_output_stt, outputs=label_output_sentiment)

demo.launch()

函数输入:列表 vs. 字典 (Function Inputs: List vs. Dict)

当事件监听器函数需要多个输入组件的值时,您的函数有两种方式接收这些值:

  1. 作为参数列表 (list of arguments)。
  2. 作为单个字典 (dictionary),其中键是组件对象,值是组件的值。
python
import gradio as gr

with gr.Blocks() as demo:
    a = gr.Number(label="a")
    b = gr.Number(label="b")
    
    with gr.Row():
        add_button = gr.Button("Add")
        subtract_button = gr.Button("Subtract")
        
    c = gr.Number(label="sum")

    def add_numbers(num1, num2):
        return num1 + num2
    add_button.click(fn=add_numbers, inputs=[a, b], outputs=c)

    def subtract_numbers_dict(data):
        return data[a] - data[b]
    # 注意 inputs={a, b} 是一个集合,它会被转换为字典
    subtract_button.click(fn=subtract_numbers_dict, inputs={a, b}, outputs=c)

demo.launch()
  • 对于 add_button,我们将输入作为列表 [a, b] 传递。函数 add_numbers 按顺序接收这些输入作为参数 num1num2
  • 对于 subtract_button,我们将输入作为集合 {a, b} 传递 (Gradio 内部会将其解释为字典形式的 inputs={a:a, b:b} 效果,函数接收时是 data[<Component a>]data[<Component b>])。函数 subtract_numbers_dict 接收一个名为 data 的字典参数,其中键是输入组件对象,值是这些组件的当前值。

对于具有许多输入组件的函数,字典语法可能更易于管理。

函数输出:列表 vs. 字典 (Function Outputs: List vs. Dict)

类似地,您可以为多个输出组件返回值:

  1. 作为值的列表 (list of values)。
  2. 作为字典 (dictionary),其中键是组件对象,值是它们的新值。

返回值的列表(按顺序对应输出组件):

python
import gradio as gr

with gr.Blocks() as demo:
    food_box = gr.Number(value=10, label="Food Count")
    status_box = gr.Textbox(label="Status")

    def eat_food(current_food):
        if current_food > 0:
            return current_food - 1, "full"
        else:
            return 0, "hungry"

    gr.Button("Eat").click(
        fn=eat_food,
        inputs=food_box,
        outputs=[food_box, status_box]
    )
demo.launch()

在这里,eat_food 函数返回两个值,分别对应 food_boxstatus_box

返回字典(允许跳过某些组件的更新):

python
import gradio as gr

with gr.Blocks() as demo:
    food_box = gr.Number(value=10, label="Food Count")
    status_box = gr.Textbox(label="Status")

    def eat_food_dict(current_food):
        if current_food > 0:
            return {food_box: current_food - 1, status_box: "full"}
        else:
            # 只更新 status_box,food_box 保持不变(如果使用 gr.skip() 则更明确)
            # 或者,如果 food_box 不在返回的字典中,Gradio 也会跳过对它的更新
            return {status_box: "hungry"}


    gr.Button("Eat").click(
        fn=eat_food_dict,
        inputs=food_box,
        outputs=[food_box, status_box] # 必须声明所有可能的输出组件
    )
demo.launch()

注意当没有食物时,我们只更新了 status_box。我们跳过了对 food_box 组件的更新,因为它没有包含在返回的字典的键中。 当事件监听器影响许多组件,或有条件地影响某些输出而不影响其他输出时,字典返回非常有用。 请记住,即使使用字典返回,我们也需要在事件监听器中指定所有可能的输出组件。

更新组件配置 (Updating Component Configurations)

事件监听函数的返回值通常是相应输出组件的更新值。有时,我们希望同时更新组件的配置,例如其可见性、行数等。在这种情况下,我们返回一个新的组件实例,设置我们想要更改的属性。

python
import gradio as gr

def change_textbox_config(choice):
    if choice == "short":
        return gr.Textbox(lines=2, visible=True, placeholder="Short essay...")
    elif choice == "long":
        return gr.Textbox(lines=8, visible=True, value="Lorem ipsum dolor sit amet", placeholder="Long essay...")
    else:
        return gr.Textbox(visible=False)

with gr.Blocks() as demo:
    radio_choice = gr.Radio(
        ["short", "long", "none"], label="What kind of essay would you like to write?"
    )
    text_editor = gr.Textbox(lines=2, interactive=True, show_copy_button=True)
    
    radio_choice.change(fn=change_textbox_config, inputs=radio_choice, outputs=text_editor)

demo.launch()

可以看到,我们可以通过返回一个新的 gr.Textbox(...) 实例来配置文本框本身。value= 参数仍然可以用来更新值以及组件配置。任何我们没有设置的参数将保留它们之前的值。

不改变组件值 (Not Changing a Component's Value)

在某些情况下,您可能希望保持组件的值不变。Gradio 包含一个特殊的值 gr.skip(),您可以从函数中返回它。返回 gr.skip() 将使输出组件(或多个组件)的值保持原样。

python
import random
import gradio as gr

with gr.Blocks() as demo:
    with gr.Row():
        clear_button = gr.Button("Clear")
        skip_button = gr.Button("Skip Update") # Renamed for clarity
        random_button = gr.Button("Random")
    
    number_outputs = [gr.Number(label="Num 1"), gr.Number(label="Num 2")]

    clear_button.click(lambda : (None, None), outputs=number_outputs)
    skip_button.click(lambda : (gr.skip(), gr.skip()), outputs=number_outputs)
    random_button.click(lambda : (random.randint(0, 100), random.randint(0, 100)), outputs=number_outputs)

demo.launch()

注意返回 None(通常会将组件的值重置为空状态)与返回 gr.skip()(保持组件值不变)之间的区别。

提示:如果您有多个输出组件,并且希望保持所有组件的值不变,您可以只返回一个 gr.skip(),而不是为每个元素返回一个包含 gr.skip() 的元组。Gradio 会将单个 gr.skip() 应用于所有列出的输出组件。

连续运行事件 (Running Events Consecutively)

您可以使用事件监听器的 .then() 方法来连续运行事件。这将在前一个事件完成后运行下一个事件。这对于分多步更新组件的事件非常有用。

例如,在下面的聊天机器人示例中,我们首先立即用用户消息更新聊天机器人,然后在模拟延迟后用计算机响应更新聊天机器人。

python
import gradio as gr
import random
import time

with gr.Blocks() as demo:
    chatbot_display = gr.Chatbot()
    message_input = gr.Textbox(label="Your message")
    clear_button = gr.Button("Clear Chat")

    def user_sends_message(user_message, history):
        return "", history + [[user_message, None]]

    def bot_responds(history):
        bot_message = random.choice(["How are you?", "I love you", "I'm very hungry"])
        time.sleep(1) # Simulate delay
        history[-1][1] = bot_message
        return history

    message_input.submit(
        fn=user_sends_message, 
        inputs=[message_input, chatbot_display], 
        outputs=[message_input, chatbot_display], 
        queue=False # For responsiveness
    ).then(
        fn=bot_responds, 
        inputs=chatbot_display, 
        outputs=chatbot_display
    )

    clear_button.click(lambda: None, outputs=chatbot_display, queue=False)

demo.launch()

事件监听器的 .then() 方法无论前一个事件是否引发任何错误,都会执行后续事件。如果您只想在先前事件成功执行后才运行后续事件,请使用 .success() 方法,它接受与 .then() 相同的参数。

将多个触发器绑定到一个函数 (Binding Multiple Triggers to a Function)

通常,您可能希望将多个触发器绑定到同一个函数。例如,您可能希望允许用户单击提交按钮或按 Enter 键来提交表单。您可以使用 gr.on() 方法,并将触发器列表传递给 triggers 参数。

python
import gradio as gr

with gr.Blocks() as demo:
    name_input = gr.Textbox(label="Name")
    output_display = gr.Textbox(label="Output Box")
    greet_button = gr.Button("Greet")
    
    # This textbox is just to show event data target
    trigger_info_display = gr.Textbox(label="Trigger Source Info") 

    def greet_user(name_val, evt_data: gr.EventData):
        # evt_data.target can give info about the component that triggered the event
        return f"Hello {name_val}!", f"Triggered by: {evt_data.target.__class__.__name__}"

    def clear_name_field(evt_data: gr.EventData): # evt_data is optional here
        return ""

    gr.on(
        triggers=[name_input.submit, greet_button.click],
        fn=greet_user,
        inputs=name_input,
        outputs=[output_display, trigger_info_display],
    ).then(fn=clear_name_field, outputs=[name_input]) # Clear name field after greeting

demo.launch()

您也可以使用装饰器语法:

python
import gradio as gr

with gr.Blocks() as demo:
    name_input = gr.Textbox(label="Name")
    output_display = gr.Textbox(label="Output Box")
    greet_button = gr.Button("Greet")

    @gr.on(triggers=[name_input.submit, greet_button.click], inputs=name_input, outputs=output_display)
    def greet_user_decorator(name_val):
        return f"Hello {name_val}!"

demo.launch()

您可以使用 gr.on 通过绑定到实现了 change 事件的组件的该事件来创建"实时"事件。如果您没有指定任何触发器,该函数将自动绑定到所有具有 change 事件的输入组件的 change 事件(例如 gr.Textboxchange 事件,而 gr.Button 没有)。

python
import gradio as gr

with gr.Blocks() as demo:
    with gr.Row():
        slider1 = gr.Slider(1, 10, label="Num 1")
        slider2 = gr.Slider(1, 10, label="Num 2")
        slider3 = gr.Slider(1, 10, label="Num 3")
    sum_output = gr.Number(label="Sum")

    # Will trigger on change of any slider if triggers is not specified
    # Or explicitly: @gr.on(triggers=[slider1.change, slider2.change, slider3.change], ...)
    @gr.on(inputs=[slider1, slider2, slider3], outputs=sum_output)
    def calculate_sum(a, b, c):
        return a + b + c

demo.launch()

您可以像常规事件监听器一样,在 gr.on 后面跟上 .then。这个方便的方法应该能帮您省去编写大量重复代码的麻烦!

直接绑定组件值 (Binding a Component Value Directly to a Function of Other Components)

如果您希望将一个组件的值设置为始终是其他组件值的函数计算结果,可以使用以下简写方式:

python
import gradio as gr

with gr.Blocks() as demo:
  num1 = gr.Number(label="Number 1")
  num2 = gr.Number(label="Number 2")
  # product's value will automatically update when num1 or num2 changes
  product = gr.Number(lambda a, b: a * b, inputs=[num1, num2], label="Product")

demo.launch()

这在功能上等同于:

python
import gradio as gr

with gr.Blocks() as demo:
  num1 = gr.Number(label="Number 1")
  num2 = gr.Number(label="Number 2")
  product = gr.Number(label="Product")

  def update_product(a, b):
      return a * b
  
  # Bind to change events of inputs and also demo.load to initialize
  gr.on(
    triggers=[num1.change, num2.change, demo.load], 
    fn=update_product, 
    inputs=[num1, num2], 
    outputs=product
  )

demo.launch()

这种简写方式使得创建响应式 UI 元素更加便捷。